var Kibo = function(element) {
	this.element = element || window.document;
	this.initialize();
};

Kibo.KEY_NAMES_BY_CODE = {
	8 : 'backspace',
	9 : 'tab',
	13 : 'enter',
	16 : 'shift',
	17 : 'ctrl',
	18 : 'alt',
	20 : 'caps_lock',
	27 : 'esc',
	32 : 'space',
	33 : 'page_up',
	34 : 'page_down',
	35 : 'end',
	36 : 'home',
	37 : 'left',
	38 : 'up',
	39 : 'right',
	40 : 'down',
	45 : 'insert',
	46 : 'delete',
	48 : '0',
	49 : '1',
	50 : '2',
	51 : '3',
	52 : '4',
	53 : '5',
	54 : '6',
	55 : '7',
	56 : '8',
	57 : '9',
	65 : 'a',
	66 : 'b',
	67 : 'c',
	68 : 'd',
	69 : 'e',
	70 : 'f',
	71 : 'g',
	72 : 'h',
	73 : 'i',
	74 : 'j',
	75 : 'k',
	76 : 'l',
	77 : 'm',
	78 : 'n',
	79 : 'o',
	80 : 'p',
	81 : 'q',
	82 : 'r',
	83 : 's',
	84 : 't',
	85 : 'u',
	86 : 'v',
	87 : 'w',
	88 : 'x',
	89 : 'y',
	90 : 'z',
	112 : 'f1',
	113 : 'f2',
	114 : 'f3',
	115 : 'f4',
	116 : 'f5',
	117 : 'f6',
	118 : 'f7',
	119 : 'f8',
	120 : 'f9',
	121 : 'f10',
	122 : 'f11',
	123 : 'f12',
	144 : 'num_lock'
};

Kibo.KEY_CODES_BY_NAME = {};
(function() {
	for (var key in Kibo.KEY_NAMES_BY_CODE)
		if (Object.prototype.hasOwnProperty.call(Kibo.KEY_NAMES_BY_CODE, key))
			Kibo.KEY_CODES_BY_NAME[Kibo.KEY_NAMES_BY_CODE[key]] = +key;
})();

Kibo.MODIFIERS = ['shift', 'ctrl', 'alt'];

Kibo.WILDCARD_TYPES = ['arrow', 'number', 'letter', 'f'];

Kibo.WILDCARDS = {
	arrow : [37, 38, 39, 40],
	number : [48, 49, 50, 51, 52, 53, 54, 55, 56, 57],
	letter : [65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80,
			81, 82, 83, 84, 85, 86, 87, 88, 89, 90],
	f : [112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123]
};

Kibo.assert = function(expression, exception) {
	exception = exception || {};
	exception.name = exception.name || 'Exception raised';
	exception.message = exception.message || 'an error has occurred.';

	try {
		if (!expression)
			throw (exception);
	} catch (error) {
		if ((typeof console !== 'undefined') && console.log)
			console.log(error.name + ': ' + error.message);
		else
			window.alert(error.name + ': ' + error.message);
	}
};

Kibo.registerEvent = (function() {
	if (document.addEventListener) {
		return function(element, eventName, func) {
			element.addEventListener(eventName, func, false);
		};
	} else if (document.attachEvent) {
		return function(element, eventName, func) {
			element.attachEvent('on' + eventName, func);
		};
	}
})();

Kibo.unregisterEvent = (function() {
	if (document.removeEventListener) {
		return function(element, eventName, func) {
			element.removeEventListener(eventName, func, false);
		};
	} else if (document.detachEvent) {
		return function(element, eventName, func) {
			element.detachEvent('on' + eventName, func);
		};
	}
})();

Kibo.isArray = function(what) {
	return !!(what && what.splice);
};

Kibo.isString = function(what) {
	return typeof what === 'string';
};

Kibo.arrayIncludes = (function() {
	if (Array.prototype.indexOf) {
		return function(haystack, needle) {
			return haystack.indexOf(needle) !== -1;
		};
	} else {
		return function(haystack, needle) {
			for (var i = 0; i < haystack.length; i++)
				if (haystack[i] === needle)
					return true;
			return false;
		};
	}
})();

Kibo.trimString = function(string) {
	return string.replace(/^\s+|\s+$/g, '');
};

Kibo.neatString = function(string) {
	return Kibo.trimString(string).replace(/\s+/g, ' ');
};

Kibo.capitalize = function(string) {
	return string.toLowerCase().replace(/^./, function(match) {
				return match.toUpperCase();
			});
};

Kibo.isModifier = function(key) {
	return Kibo.arrayIncludes(Kibo.MODIFIERS, key);
};

Kibo.prototype.initialize = function() {
	var i, that = this;

	this.lastKeyCode = -1;
	this.lastModifiers = {};
	for (i = 0; i < Kibo.MODIFIERS.length; i++)
		this.lastModifiers[Kibo.MODIFIERS[i]] = false;

	this.keysDown = {
		any : []
	};
	this.keysUp = {
		any : []
	};
	for (i = 0; i < Kibo.WILDCARD_TYPES.length; i++) {
		this.keysDown['any ' + Kibo.WILDCARD_TYPES[i]] = [];
		this.keysUp['any ' + Kibo.WILDCARD_TYPES[i]] = [];
	}

	this.downHandler = this.handler('down');
	this.upHandler = this.handler('up');

	Kibo.registerEvent(this.element, 'keydown', this.downHandler);
	Kibo.registerEvent(this.element, 'keyup', this.upHandler);
	Kibo.registerEvent(window, 'unload', function unloader() {
				Kibo.unregisterEvent(that.element, 'keydown', that.downHandler);
				Kibo.unregisterEvent(that.element, 'keyup', that.upHandler);
				Kibo.unregisterEvent(window, 'unload', unloader);
			});
};

Kibo.prototype.matchingKeys = function(registeredKeys) {
	var i, j, keyCombination, match, result = [];
	for (var registeredKey in registeredKeys) {
		if (Object.prototype.hasOwnProperty.call(registeredKeys, registeredKey)) {
			keyCombination = Kibo.trimString(registeredKey).split(' ');
			if (keyCombination.length && keyCombination[0] !== 'any') {
				match = true;
				for (j = 0; j < keyCombination.length; j++)
					match = match
							&& (Kibo.isModifier(keyCombination[j]) ? this
									.lastKey(keyCombination[j]) : (this
									.lastKey() === keyCombination[j]));
				if (match)
					result.push(registeredKey);
			}
		}
	}
	return result;
};

Kibo.prototype.handler = function(upOrDown) {
	var that = this;
	return function(e) {
		var i, j, matchingKeys, registeredKeys;

		e = e || window.event;

		that.lastKeyCode = e.keyCode;
		for (i = 0; i < Kibo.MODIFIERS.length; i++)
			that.lastModifiers[Kibo.MODIFIERS[i]] = e[Kibo.MODIFIERS[i] + 'Key'];
		if (Kibo.arrayIncludes(Kibo.MODIFIERS, Kibo.keyName(that.lastKeyCode)))
			that.lastModifiers[Kibo.keyName(that.lastKeyCode)] = true;

		registeredKeys = that['keys' + Kibo.capitalize(upOrDown)];
		matchingKeys = that.matchingKeys(registeredKeys);

		for (i = 0; i < registeredKeys.any.length; i++)
			if ((registeredKeys.any[i](e) === false) && e.preventDefault)
				e.preventDefault();

		for (i = 0; i < Kibo.WILDCARD_TYPES.length; i++)
			if (Kibo.arrayIncludes(Kibo.WILDCARDS[Kibo.WILDCARD_TYPES[i]],
					that.lastKeyCode))
				for (j = 0; j < registeredKeys['any ' + Kibo.WILDCARD_TYPES[i]].length; j++)
					if ((registeredKeys['any ' + Kibo.WILDCARD_TYPES[i]][j](e) === false)
							&& e.preventDefault)
						e.preventDefault();

		for (i = 0; i < matchingKeys.length; i++)
			for (j = 0; j < registeredKeys[matchingKeys[i]].length; j++)
				if ((registeredKeys[matchingKeys[i]][j](e) === false)
						&& e.preventDefault)
					e.preventDefault();
	};
};

Kibo.prototype.registerKeys = function(upOrDown, newKeys, func) {
	var i, registeredKeys = this['keys' + Kibo.capitalize(upOrDown)];

	if (!Kibo.isArray(newKeys))
		newKeys = [newKeys];

	for (i = 0; i < newKeys.length; i++) {
		Kibo.assert(Kibo.isString(newKeys[i]), {
					name : 'Type error',
					message : 'expected string or array of strings.'
				});

		newKeys[i] = Kibo.neatString(newKeys[i]);

		if (Kibo.isArray(registeredKeys[newKeys[i]]))
			registeredKeys[newKeys[i]].push(func);
		else
			registeredKeys[newKeys[i]] = [func];
	}

	return this;
};

Kibo.prototype.unregisterKeys = function(upOrDown, newKeys, func) {
	var i, j, registeredKeys = this['keys' + Kibo.capitalize(upOrDown)];

	if (!Kibo.isArray(newKeys))
		newKeys = [newKeys];

	for (i = 0; i < newKeys.length; i++) {
		Kibo.assert(Kibo.isString(newKeys[i]), {
					name : 'Type error',
					message : 'expected string or array of strings.'
				});

		newKeys[i] = Kibo.neatString(newKeys[i]);

		if (func === null)
			delete registeredKeys[newKeys[i]];
		else {
			if (Kibo.isArray(registeredKeys[newKeys[i]])) {
				for (j = 0; j < registeredKeys[newKeys[i]].length; j++) {
					if (String(registeredKeys[newKeys[i]][j]) === String(func)) {
						registeredKeys[newKeys[i]].splice(j, 1);
						break;
					}
				}
			}
		}
	}

	return this;
};

Kibo.prototype.delegate = function(action, keys, func) {
	return func !== null ? this.registerKeys(action, keys, func) : this
			.unregisterKeys(action, keys, func);
};
Kibo.prototype.down = function(keys, func) {
	return this.delegate('down', keys, func);
};

Kibo.prototype.up = function(keys, func) {
	return this.delegate('up', keys, func);
};

Kibo.keyName = function(keyCode) {
	return Kibo.KEY_NAMES_BY_CODE[keyCode + ''];
};

Kibo.keyCode = function(keyName) {
	return +Kibo.KEY_CODES_BY_NAME[keyName];
};

Kibo.prototype.lastKey = function(modifier) {
	if (!modifier)
		return Kibo.keyName(this.lastKeyCode);

	Kibo.assert(Kibo.arrayIncludes(Kibo.MODIFIERS, modifier), {
				name : 'Modifier error',
				message : 'invalid modifier ' + modifier
						+ ' (valid modifiers are: ' + Kibo.MODIFIERS.join(', ')
						+ ').'
			});

	return this.lastModifiers[modifier];
};